<template>
  <div>
    <header>
      <el-button
        type="primary"
        :disabled="selectedTextList.length === 0"
        plain
        @click="handleAllDelete"
      >
        清空标记
      </el-button>
      <el-button type="primary" :disabled="selectedTextList.length === 0" plain @click="handleSave">
        保存
      </el-button>
    </header>
    <main>
      <div ref="containerRef" id="text-container" class="text">
        {{ markContent }}
      </div>
      <div
        v-if="tagInfo.visible && tagList.length > 0"
        :class="['tag-box', 'p-4']"
        :style="{
          top: tagInfo.top + 'px',
          left: tagInfo.left + 'px',
        }"
      >
        <ClickOutSide @click-outside="handleClickOutside">
          <div v-for="i in tagList" :key="i.tag_id" class="tag-name" @click="handleSelectLabel(i)">
            <el-space>
              <p>{{ i.tag_name }}</p>
              <el-button v-if="i.tag_id === editTag.tag_id" text type="primary">√</el-button>
            </el-space>
            <div :class="['w-4', 'h-4']" :style="{ background: i.tag_color }"></div>
          </div>
        </ClickOutSide>
      </div>
      <div
        v-if="editTag.visible"
        class="edit-tag"
        :style="{ top: editTag.top + 'px', left: editTag.left + 'px' }"
      >
        <ClickOutSide @click-outside="handleClickOutside">
          <div class="py-1 bg-gray-100 text-center" @click="handleCancel">清 除</div>
          <div class="py-1 bg-gray-100 mt-2 text-center" @click="handleReset">重 选</div>
        </ClickOutSide>
      </div>
    </main>
  </div>
</template>

<script setup>
  import { ref, onMounted } from 'vue';
  import { nanoid } from 'nanoid';
  import { ClickOutSide } from '@/components/ClickOutSide';
  import Mark from 'mark.js';

  const containerRef = ref(null);
  const selectedTextList = ref([
    {
      start: 18,
      end: 31,
      length: 13,
      tag_color: '#DE050CFF',
      tag_id: 'tag_id1',
      tag_name: '标签一',
      mark_content: '是一种具有函数优先的轻量级',
      mark_id: 'mark_id1',
    },
    {
      start: 87,
      end: 102,
      length: 15,
      tag_color: '#6ADE05FF',
      tag_id: 'tag_id2',
      tag_name: '标签二',
      mark_content: 'JavaScript于原型编程',
      mark_id: 'mark_id4',
    },
  ]);
  const selectedText = ref({
    start: 0,
    end: 0,
    content: '',
  });
  const markContent = ref(
    'JavaScript（简称“JS”）是一种具有函数优先的轻量级，解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名，但是它也被用到了很多非浏览器环境中，JavaScript于原型编程、多范式的动态脚本语言，并且支持面向对象、命令式、声明式、函数式编程范式。',
  );
  const tagInfo = ref({
    visible: false,
    top: 0,
    left: 0,
  });
  const editTag = ref({
    visible: false,
    top: 0,
    left: 0,
    mark_id: '',
    content: '',
    tag_id: '',
    start: 0,
    end: 0,
  });
  const tagList = [
    {
      tag_name: '标签一',
      tag_color: `#DE050CFF`,
      tag_id: 'tag_id1',
    },
    {
      tag_name: '标签二',
      tag_color: `#6ADE05FF`,
      tag_id: 'tag_id2',
    },
    {
      tag_name: '标签三',
      tag_color: `#DE058BFF`,
      tag_id: 'tag_id3',
    },
    {
      tag_name: '标签四',
      tag_color: `#9205DEFF`,
      tag_id: 'tag_id4',
    },
    {
      tag_name: '标签五',
      tag_color: `#DE5F05FF`,
      tag_id: 'tag_id5',
    },
  ];

  function flattenChildNodes(nodeList, ret) {
    for (let i = 0; i < nodeList.length; i++) {
      const node = nodeList[i];
      const { childNodes } = node;
      if (!childNodes || childNodes.length <= 1) {
        ret.push(node);
      } else {
        flattenChildNodes(childNodes, ret);
      }
    }
  }

  function highlightHelper(newInfo) {
    const { start, end, length, tag_id, tag_color, mark_id, mark_content } = newInfo;
    const marker = new Mark(containerRef.value);
    marker.markRanges([{ start, length }], {
      element: 'span',
      className: 'text-selected',
      each(element) {
        element.setAttribute('id', mark_id);
        element.style.borderBottom = `2px solid ${tag_color}`;
        element.style.color = tag_color;
        element.style.cursor = 'pointer';
        element.style.paddingBottom = `${Number.parseInt(Math.random() * 10, 10)}px`;
        element.onclick = (e) => {
          setTimeout(() => {
            editTag.value = {
              visible: true,
              top: e.offsetY + 60,
              left: e.offsetX,
              mark_id: mark_id,
              content: mark_content,
              tag_id: tag_id,
              start: start,
              end: end,
            };
            tagInfo.value = {
              visible: false,
              top: e.offsetY + 60,
              left: e.offsetX,
            };
            window?.getSelection().empty();
          });
        };
      },
    });
  }

  function handleAllDelete() {
    selectedTextList.value = [];
    const marker = new Mark(containerRef.value);
    marker.unmark();
  }

  function handleCancel() {
    if (!editTag.value.mark_id) {
      return;
    }
    const markEl = new Mark(document.getElementById(editTag.value.mark_id));
    markEl.unmark();
    selectedTextList.value.splice(
      selectedTextList.value.findIndex((t) => t.mark_id === editTag.value.mark_id),
      1,
    );
    tagInfo.value = {
      visible: false,
      top: 0,
      left: 0,
    };
    resetEditTag();
  }

  function handleReset() {
    setTimeout(() => {
      editTag.value.visible = false;
      tagInfo.value.visible = true;
    });
  }

  function handleSave() {
    console.log('标注的数据', selectedTextList.value);
  }

  function handleSelectLabel(t) {
    const { tag_color, tag_name, tag_id } = t;
    tagInfo.value.visible = false;
    const marker = new Mark(containerRef.value);
    const markId = nanoid(10);
    const isReset = selectedTextList.value.map((j) => j.mark_id).includes(editTag.value.mark_id)
      ? 1
      : 0; // 1:重选 0:新增
    if (isReset) {
      const markEl = new Mark(document.getElementById(editTag.value.mark_id));
      markEl.unmark();
      selectedTextList.value.splice(
        selectedTextList.value.findIndex((t) => t.mark_id === editTag.value.mark_id),
        1,
      );
    }
    marker.markRanges(
      [
        {
          start: isReset ? editTag.value.start : selectedText.value.start,
          length: isReset ? editTag.value.content.length : selectedText.value.content.length,
        },
      ],
      {
        element: 'span',
        className: 'text-selected',
        each(element) {
          element.setAttribute('id', markId);
          element.style.borderBottom = `2px solid ${tag_color}`;
          element.style.color = tag_color;
          element.style.cursor = 'pointer';
          element.style.paddingBottom = `${Number.parseInt(Math.random() * 10, 10)}px`;
          element.onclick = function (e) {
            e.preventDefault();
            if (!e.target.id) {
              return;
            }
            const item = selectedTextList.value.find((t) => t.mark_id === e.target.id);
            const { mark_content, tag_id, start, end } = item;
            setTimeout(() => {
              editTag.value = {
                visible: true,
                top: e.offsetY + 60,
                left: e.offsetX,
                mark_id: e.target.id,
                content: mark_content || '',
                tag_id: tag_id || '',
                start: start,
                end: end,
              };
              tagInfo.value = {
                visible: false,
                top: e.offsetY + 60,
                left: e.offsetX,
              };
              window?.getSelection().empty();
            });
          };
        },
      },
    );
    selectedTextList.value.push({
      tag_color,
      tag_name,
      tag_id,
      start: isReset ? editTag.value.start : selectedText.value.start,
      end: isReset ? editTag.value.end : selectedText.value.end,
      mark_content: isReset ? editTag.value.content : selectedText.value.content,
      mark_id: markId,
    });
  }

  function getSelectedTextData() {
    const selection = window.getSelection();
    const text = selection.toString();
    const range = selection.getRangeAt(0);
    const { startContainer, endContainer, startOffset, endOffset } = range;

    let startIdx = 0;
    let endIdx = 0;
    let isFinishStart = false;
    let isFinishEnd = false;
    const childNodes = [];
    flattenChildNodes(containerRef.value.childNodes, childNodes);

    for (let child of childNodes) {
      const isTextNode = child.tagName == null;
      const isStartContainer = child === startContainer || child === startContainer.parentElement;
      const isEndContainer = child === endContainer || child === endContainer.parentElement;
      const childLength = isTextNode ? child.length : child.textContent.length;
      if (!isFinishStart && !isStartContainer) {
        startIdx += childLength;
        endIdx += childLength;
      } else if (!isFinishStart && isStartContainer && !isEndContainer) {
        startIdx += startOffset;
        endIdx += childLength;
        isFinishStart = true;
      } else if (!isFinishStart && isStartContainer && isEndContainer) {
        startIdx += startOffset;
        endIdx += endOffset;
        isFinishStart = true;
        isFinishEnd = true;
      } else if (isFinishStart && !isFinishEnd && !isEndContainer) {
        endIdx += childLength;
      } else if (isFinishStart && !isFinishEnd && isEndContainer) {
        endIdx += endOffset;
        isFinishEnd = true;
      }
    }

    selectedText.value.start = startIdx;
    selectedText.value.end = endIdx;
    selectedText.value.content = text;
  }

  function resetEditTag() {
    editTag.value = {
      visible: false,
      top: 0,
      left: 0,
      mark_id: '',
      content: '',
      tag_id: '',
      start: 0,
      end: 0,
    };
  }

  function handleClickOutside() {
    tagInfo.value.visible = false;
    editTag.value.visible = false;
  }

  // eslint-disable-next-line
  function getColor() {
    const r1 = Number.parseInt(Math.random() * 256, 10);
    const r2 = Number.parseInt(Math.random() * 256, 10);
    const r3 = Number.parseInt(Math.random() * 256, 10);
    return `rgb(${r1}, ${r2}, ${r3})`;
  }

  onMounted(() => {
    containerRef.value.addEventListener('mouseup', (e) => {
      const text = window?.getSelection()?.toString() || '';
      if (text.length > 0) {
        setTimeout(() => {
          tagInfo.value = {
            visible: true,
            top: e.offsetY + 60,
            left: e.offsetX,
          };
        });
        getSelectedTextData();
      } else {
        tagInfo.value.visible = false;
      }
      resetEditTag();
    });

    selectedTextList.value.forEach((m) => {
      highlightHelper(m);
    });
  });
</script>

<style lang="scss">
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 80px;
    padding: 0 24px;
    border-bottom: 1px solid #e5e7eb;
    background: #fff;
    user-select: none;
  }

  main {
    position: relative;
    height: 80vh;
    margin: 24px;
    padding: 24px;
    overflow-y: auto;
    background: #fff;
    box-shadow: 0 3px 8px 0 rgb(0 0 0 / 13%);

    .text {
      color: #333;
      font-size: 16px;
      font-weight: 500;
      line-height: 50px;

      .text-selected {
        &:hover {
          border: 1px solid black;
        }
      }
    }

    .tag-box {
      position: absolute;
      z-index: 10;
      width: 280px;
      max-height: 40vh;
      overflow-y: auto;
      border-radius: 4px;
      background: #fff;
      box-shadow:
        0 9px 28px 8px rgb(0 0 0 / 3%),
        0 6px 16px 4px rgb(0 0 0 / 9%),
        0 3px 6px -2px rgb(0 0 0 / 20%);
      user-select: none;

      .tag-name {
        display: flex;
        align-items: center;
        justify-content: space-between;
        width: 100%;
        margin-top: 8px;
        padding: 4px 8px;
        background: rgb(243 244 246 / var(--tw-bg-opacity));
        font-size: 14px;
        cursor: pointer;
      }

      .tag-name:nth-of-type(1) {
        margin-top: 0;
      }
    }

    .edit-tag {
      position: absolute;
      z-index: 20;
      width: 100px;
      padding: 16px;
      border-radius: 4px;
      background: #fff;
      box-shadow:
        0 9px 28px 8px rgb(0 0 0 / 3%),
        0 6px 16px 4px rgb(0 0 0 / 9%),
        0 3px 6px -2px rgb(0 0 0 / 20%);
      cursor: pointer;
      user-select: none;
    }

    ::selection {
      background: rgb(51 51 51 / 20%);
    }
  }
</style>
